from typing import Any

import pytest
from django.conf import settings
from django.http import HttpRequest, HttpResponse
from django.template import Context, Template
from django.test import Client
from django.urls import path
from pytest_django.asserts import assertInHTML

from django_components import Component, get_component_url, register, types
from django_components.testing import djc_test
from django_components.urls import urlpatterns as dc_urlpatterns
from django_components.util.misc import format_url

from .testutils import setup_test_config


# DO NOT REMOVE!
#
# This is intentionally defined before `setup_test_config()` in order to test that
# the URL extension works even before the Django has been set up.
#
# Because if we define the component before `django.setup()`, then we store it in
# event queue, and will register it when `AppConfig.ready()` is finally called.
#
# This test relies on the "url" extension calling `add_extension_urls()` from within
# the `on_component_class_created()` hook.
class ComponentBeforeReady(Component):
    class View:
        public = True

    template = "Hello"


setup_test_config()


class CustomClient(Client):
    def __init__(self, urlpatterns=None, *args, **kwargs):
        import types

        if urlpatterns:
            urls_module = types.ModuleType("urls")
            urls_module.urlpatterns = urlpatterns + dc_urlpatterns  # type: ignore[attr-defined]
            settings.ROOT_URLCONF = urls_module
        else:
            settings.ROOT_URLCONF = __name__
        settings.SECRET_KEY = "secret"  # noqa: S105
        super().__init__(*args, **kwargs)


@djc_test
class TestComponentAsView:
    def test_render_component_from_template(self):
        @register("testcomponent")
        class MockComponentRequest(Component):
            template = """
                <form method="post">
                    {% csrf_token %}
                    <input type="text" name="variable" value="{{ variable }}">
                    <input type="submit">
                </form>
                """

            def get_template_data(self, args, kwargs, slots, context):
                return {"variable": kwargs["variable"]}

        def render_template_view(_request):
            template = Template(
                """
                {% load component_tags %}
                {% component "testcomponent" variable="TEMPLATE" %}{% endcomponent %}
                """,
            )
            return HttpResponse(template.render(Context({})))

        client = CustomClient(urlpatterns=[path("test_template/", render_template_view)])
        response = client.get("/test_template/")
        assert response.status_code == 200
        assertInHTML(
            '<input type="text" name="variable" value="TEMPLATE">',
            response.content.decode(),
        )

    def test_get_request(self):
        class MockComponentRequest(Component):
            template = """
                <form method="post">
                    {% csrf_token %}
                    <input type="text" name="variable" value="{{ inner_var }}">
                    <input type="submit">
                </form>
                """

            def get_template_data(self, args, kwargs, slots, context):
                return {"inner_var": kwargs["variable"]}

            class View:
                def get(self, request, *args, **kwargs) -> HttpResponse:
                    return self.component_cls.render_to_response(kwargs={"variable": "GET"})  # type: ignore[attr-defined]

        client = CustomClient(urlpatterns=[path("test/", MockComponentRequest.as_view())])
        response = client.get("/test/")
        assert response.status_code == 200
        assertInHTML(
            '<input type="text" name="variable" value="GET">',
            response.content.decode(),
        )

    def test_get_request_shortcut(self):
        class MockComponentRequest(Component):
            template = """
                <form method="post">
                    {% csrf_token %}
                    <input type="text" name="variable" value="{{ inner_var }}">
                    <input type="submit">
                </form>
                """

            def get_template_data(self, args, kwargs, slots, context):
                return {"inner_var": kwargs["variable"]}

            def get(self, request, *args, **kwargs) -> HttpResponse:
                return self.render_to_response(kwargs={"variable": "GET"})

        client = CustomClient(urlpatterns=[path("test/", MockComponentRequest.as_view())])
        response = client.get("/test/")
        assert response.status_code == 200
        assertInHTML(
            '<input type="text" name="variable" value="GET">',
            response.content.decode(),
        )

    def test_post_request(self):
        class MockComponentRequest(Component):
            template: types.django_html = """
                <form method="post">
                    {% csrf_token %}
                    <input type="text" name="variable" value="{{ inner_var }}">
                    <input type="submit">
                </form>
                """

            def get_template_data(self, args, kwargs, slots, context):
                return {"inner_var": kwargs["variable"]}

            class View:
                def post(self, request, *args, **kwargs) -> HttpResponse:
                    variable = request.POST.get("variable")
                    return self.component_cls.render_to_response(kwargs={"variable": variable})  # type: ignore[attr-defined]

        client = CustomClient(urlpatterns=[path("test/", MockComponentRequest.as_view())])
        response = client.post("/test/", {"variable": "POST"})
        assert response.status_code == 200
        assertInHTML(
            '<input type="text" name="variable" value="POST">',
            response.content.decode(),
        )

    def test_post_request_shortcut(self):
        class MockComponentRequest(Component):
            template: types.django_html = """
                <form method="post">
                    {% csrf_token %}
                    <input type="text" name="variable" value="{{ inner_var }}">
                    <input type="submit">
                </form>
                """

            def get_template_data(self, args, kwargs, slots, context):
                return {"inner_var": kwargs["variable"]}

            def post(self, request, *args, **kwargs) -> HttpResponse:
                variable = request.POST.get("variable")
                return self.render_to_response(kwargs={"variable": variable})

        client = CustomClient(urlpatterns=[path("test/", MockComponentRequest.as_view())])
        response = client.post("/test/", {"variable": "POST"})
        assert response.status_code == 200
        assertInHTML(
            '<input type="text" name="variable" value="POST">',
            response.content.decode(),
        )

    def test_instantiate_component(self):
        class MockComponentRequest(Component):
            template = """
                <form method="post">
                    <input type="text" name="variable" value="{{ inner_var }}">
                </form>
                """

            def get_template_data(self, args, kwargs, slots, context):
                return {"inner_var": kwargs["variable"]}

            def get(self, request, *args, **kwargs) -> HttpResponse:
                return self.render_to_response(kwargs={"variable": self.name})

        view = MockComponentRequest.as_view()
        client = CustomClient(urlpatterns=[path("test/", view)])
        response = client.get("/test/")
        assert response.status_code == 200
        assertInHTML(
            '<input type="text" name="variable" value="MockComponentRequest">',
            response.content.decode(),
        )

    def test_replace_slot_in_view(self):
        class MockComponentSlot(Component):
            template = """
                {% load component_tags %}
                <div>
                {% slot "first_slot" %}
                    Hey, I'm {{ name }}
                {% endslot %}
                {% slot "second_slot" %}
                {% endslot %}
                </div>
                """

            def get(self, request, *args, **kwargs) -> HttpResponse:
                return self.render_to_response(
                    context={"name": "Bob"},
                    slots={"second_slot": "Nice to meet you, Bob"},
                )

        client = CustomClient(urlpatterns=[path("test_slot/", MockComponentSlot.as_view())])
        response = client.get("/test_slot/")
        assert response.status_code == 200
        assert b"Hey, I'm Bob" in response.content
        assert b"Nice to meet you, Bob" in response.content

    def test_replace_slot_in_view_with_insecure_content(self):
        class MockInsecureComponentSlot(Component):
            template = """
                {% load component_tags %}
                <div>
                {% slot "test_slot" %}
                {% endslot %}
                </div>
                """

            def get(self, request, *args, **kwargs) -> HttpResponse:
                return self.render_to_response(
                    context={},
                    slots={"test_slot": "<script>alert(1);</script>"},
                )

        client = CustomClient(urlpatterns=[path("test_slot_insecure/", MockInsecureComponentSlot.as_view())])
        response = client.get("/test_slot_insecure/")
        assert response.status_code == 200
        assert b"<script>" not in response.content

    def test_replace_context_in_view(self):
        class TestComponent(Component):
            template = """
                {% load component_tags %}
                <div>
                Hey, I'm {{ name }}
                </div>
                """

            def get(self, request, *args, **kwargs) -> HttpResponse:
                return self.render_to_response({"name": "Bob"})

        client = CustomClient(urlpatterns=[path("test_context_django/", TestComponent.as_view())])
        response = client.get("/test_context_django/")
        assert response.status_code == 200
        assert b"Hey, I'm Bob" in response.content

    def test_replace_context_in_view_with_insecure_content(self):
        class MockInsecureComponentContext(Component):
            template = """
                {% load component_tags %}
                <div>
                {{ variable }}
                </div>
                """

            def get(self, request, *args, **kwargs) -> HttpResponse:
                return self.render_to_response({"variable": "<script>alert(1);</script>"})

        client = CustomClient(urlpatterns=[path("test_context_insecure/", MockInsecureComponentContext.as_view())])
        response = client.get("/test_context_insecure/")
        assert response.status_code == 200
        assert b"<script>" not in response.content

    def test_component_url(self):
        class TestComponent(Component):
            template = "Hello"

            class View:
                public = True

        # Check if the URL is correctly generated
        component_url = get_component_url(TestComponent)
        assert component_url == f"/components/ext/view/components/{TestComponent.class_id}/"

        component_url2 = get_component_url(TestComponent, query={"foo": "bar"}, fragment="baz")
        assert component_url2 == f"/components/ext/view/components/{TestComponent.class_id}/?foo=bar#baz"

        # Check that the query and fragment are correctly escaped
        component_url3 = get_component_url(TestComponent, query={"f'oo": "b ar&ba'z"}, fragment='q u"x')
        assert (
            component_url3
            == f"/components/ext/view/components/{TestComponent.class_id}/?f%27oo=b+ar%26ba%27z#q%20u%22x"
        )

        # Converts `True` to no flag parameter (no value, e.g. `enabled` instead of `enabled=True`)
        # And filters out `False` and `None` values
        component_url3 = get_component_url(
            TestComponent,
            query={"f'oo": "b ar&ba'z", "true_key": True, "false_key": False, "none_key": None},
            fragment='q u"x',
        )
        assert (
            component_url3
            == f"/components/ext/view/components/{TestComponent.class_id}/?f%27oo=b+ar%26ba%27z&true_key#q%20u%22x"
        )

        # Merges query params from original URL
        component_url4 = format_url(
            "/components/ext/view/components/123?foo=123&bar=456#abc",
            query={"foo": "new", "baz": "new2"},
            fragment="xyz",
        )
        assert component_url4 == "/components/ext/view/components/123?foo=new&bar=456&baz=new2#xyz"

    def test_public_url(self):
        did_call_get = False
        did_call_post = False

        class TestComponent(Component):
            template = "Hello"

            class View:
                public = True

                def get(self, request: HttpRequest, **kwargs: Any):
                    nonlocal did_call_get
                    did_call_get = True

                    component: Component = self.component  # type: ignore[attr-defined]
                    return component.render_to_response()

                def post(self, request: HttpRequest, **kwargs: Any):
                    nonlocal did_call_post
                    did_call_post = True

                    component: Component = self.component  # type: ignore[attr-defined]
                    return component.render_to_response()

        # Check if the URL is correctly generated
        component_url = get_component_url(TestComponent)
        assert component_url == f"/components/ext/view/components/{TestComponent.class_id}/"

        client = Client()
        response = client.get(component_url)
        assert response.status_code == 200
        assert response.content == b"Hello"
        assert did_call_get

        response = client.post(component_url)
        assert response.status_code == 200
        assert response.content == b"Hello"
        assert did_call_get

    def test_public_url_implicit(self):
        class TestComponent(Component):
            template = "Hello"

            class View:
                def get(self, request: HttpRequest, **kwargs: Any):
                    component: Component = self.component  # type: ignore[attr-defined]
                    return component.render_to_response()

        # Check if the URL is correctly generated
        component_url = get_component_url(TestComponent)
        assert component_url == f"/components/ext/view/components/{TestComponent.class_id}/"

    def test_public_url_disabled(self):
        did_call_get = False

        class TestComponent(Component):
            template = "Hi"

            class View:
                public = False

                def get(self, request: HttpRequest, **attrs: Any):
                    nonlocal did_call_get
                    did_call_get = True

                    component: Component = self.component  # type: ignore[attr-defined]
                    return component.render_to_response()

        # Attempt to get the URL should raise RuntimeError
        with pytest.raises(
            RuntimeError,
            match="Component URL is not available - Component is not public",
        ):
            get_component_url(TestComponent)

        # Even calling the URL directly should raise an error
        component_url = f"/components/ext/view/components/{TestComponent.class_id}/"

        client = Client()
        response = client.get(component_url)
        assert response.status_code == 404
        assert not did_call_get
